#!/bin/bash
# ###########################
# Bash Shell Function Library
# ###########################
#
# Author: Louwrentius <louwrentius@gmail.com>
# Contributions by: Jani Hurskainen
#
# Copyright © 2010
#
# Released under the curren GPL version.
#
# Description:
#
# This is a shell script library. It contains functions that can be called by
# programs that include (source) this library. 
#
# By simply sourcing this library, you can use all available functions as 
# documented on the projects page.
#
#

BSFL_VERSION="2.00-beta-3"

#
# Do not edit this file. Just source it into your script
# and override the variables to change their value.
#

init () {

    #
    # Debug mode shows more verbose output to screen and log files.
    # Value: yes or no (y / n)
    #
    DEBUG=no
    
    # 
    # Syslog style log messages
    #
    if ! defined LOGDATEFORMAT
    then
        LOGDATEFORMAT="%b %e %H:%M:%S"
    fi
    if ! defined LOG_FILE
    then
        if [ "$0" == "bash" ]
        then
            LOG_FILE=log.txt
        else
            LOG_FILE="$0".log
        fi
    fi
    
    #
    # Enable / disable logging to a file
    # Value: yes or no (y / n)
    #
    if ! defined LOG_ENABLED
    then
        LOG_ENABLED=no
    fi
    if ! defined SYSLOG_ENABLED
    then
        SYSLOG_ENABLED=no
    fi
    if ! defined SYSLOG_TAG
    then
        SYSLOG_TAG=$0
    fi
    
    #
    # Use colours in output.
    #
    RED="tput setaf 1"
    GREEN="tput setaf 2"
    YELLOW="tput setaf 3"
    BLUE="tput setaf 4"
    MAGENTA="tput setaf 5"
    CYAN="tput setaf 6"
    BOLD="tput bold"
    DEFAULT="tput sgr0"
    
    RED_BG="tput setab 1"
    GREEN_BG="tput setab 2"
    YELLOW_BG="tput setab 3"
    BLUE_BG="tput setab 4"
    MAGENTA_BG="tput setab 5"
    CYAN_BG="tput setab 6"
    
    # 
    # Bug fix for Bash, parsing exclamation mark.
    #
    set +o histexpand
    #

    #
    # Some 'internal' BSFL specific variables.
    # 
    START_WATCH=""
}

function defined {
    [[ ${!1-X} == ${!1-Y} ]]
}

#
# returns 0 if a variable is defined (set) and value's length > 0
# returns 1 otherwise
#
function has_value {
    if defined $1; then
        if [[ -n ${!1} ]]; then
            return 0
        fi
    fi
    return 1
}

#
# returns 0 if a directory exists
# returns 1 otherwise
#
function directory_exists {
    if [[ -d "$1" ]]; then
        return 0
    fi
    return 1
}

#
# returns 0 if a (regular) file exists
# returns 1 otherwise
#
function file_exists {
    if [[ -f "$1" ]]; then
        return 0
    fi
    return 1
}

#
# returns lowercase string
#
function tolower {
    echo "$1" | tr '[:upper:]' '[:lower:]'
}

#
# returns uppercase string
#
function toupper {
    echo "$1" | tr '[:lower:]' '[:upper:]'
}

#
# Only returns the first part of a string, delimited by tabs or spaces
#
function trim {
    echo $1
}

#
# Dummy function to provide usage instructions.
# Override this function if required.
#
show_usage () {
   
    MESSAGE="$1" 
    echo "$MESSAGE"
    exit 1
}

#
# Checks if a variable is set to "y" or "yes".
# Usefull for detecting if a configurable option is set or not.
#
option_enabled () {

    VAR="$1"
    VAR_VALUE=$(eval echo \$$VAR)
    if [[ "$VAR_VALUE" == "y" ]] || [[ "$VAR_VALUE" == "yes" ]]
    then
        return 0
    else
        return 1
    fi
}

#
# The log funcion just puts a string into a file, prepended with a date & time in 
# syslog format.
#

log2syslog () {

    if option_enabled  SYSLOG_ENABLED
    then
       MESSAGE="$1"
       logger -t "$SYSLOG_TAG" " $MESSAGE" #The space is not a typo!"
    fi
}

#
# This function writes messages to a log file and/or syslog
# The only argument is a message that has to be logged.
# 

log () {
    
    if option_enabled LOG_ENABLED || option_enabled SYSLOG_ENABLED 
    then
        LOG_MESSAGE="$1" 
        STATE="$2"
        DATE=`date +"$LOGDATEFORMAT"`

        if has_value LOG_MESSAGE
        then
            LOG_STRING="$DATE $STATE - $LOG_MESSAGE"
        else
            LOG_STRING="$DATE -- empty log message, no input received --"
        fi

        if option_enabled LOG_ENABLED
        then
            echo "$LOG_STRING" >> "$LOG_FILE"
        fi

        if option_enabled SYSLOG_ENABLED
        then
            #
            # Syslog already prepends a date/time stamp so only the message 
            # is logged. 
            #
            log2syslog "$LOG_MESSAGE"
        fi
    fi
}


#
# This function basically replaces the 'echo' function in bash scripts.
# The added functionality over echo is logging and using colors. 
#
# The first argument is the string / message that must be displayed.
# The second argument is the text color.

msg () {

    MESSAGE="$1"
    COLOR="$2"

    if ! has_value COLOR
    then
        COLOR="$DEFAULT"
    fi

    if has_value "MESSAGE"
    then
        $COLOR
        echo "$MESSAGE" 
        $DEFAULT
        if ! option_enabled "DONOTLOG"
        then
            log "$MESSAGE"
        else
            echo "DONOTLOG is $DONOTLOG"
        fi
    else
        echo "-- no message received --"
        if ! option_enabled "DONOTLOG"
        then
            log "$MESSAGE"
        fi
    fi
}


#
# The log message is formatted with the status preceding the message.
#

log_status () {

    if option_enabled LOG_ENABLED
    then 
        
        MESSAGE="$1"
        STATUS="$2"
        
        log "$MESSAGE" "$STATUS"
    fi
}


#
# For every status message, a short hand log function is available.
#

log_emergency () {

    MESSAGE="$1"
    STATUS="EMERGENCY"
    log_status "$MESSAGE" "$STATUS"
}

log_alert () {

    MESSAGE="$1"
    STATUS="ALERT"
    log_status "$MESSAGE" "$STATUS"
}

log_critical () {

    MESSAGE="$1"
    STATUS="CRITICAL"
    log_status "$MESSAGE" "$STATUS"
}

log_error () {

    MESSAGE="$1"
    STATUS="ERROR"
    log_status "$MESSAGE" "$STATUS"
}

log_warning () {

    MESSAGE="$1"
    STATUS="WARNING"
    log_status "$MESSAGE" "$STATUS"
}

log_notice () {
    MESSAGE="$1"
    STATUS="NOTICE"
    log_status "$MESSAGE" "$STATUS"
}

log_info () {
    MESSAGE="$1"
    STATUS="INFO"
    log_status "$MESSAGE" "$STATUS"
}

log_debug () {
    MESSAGE="$1"
    STATUS="DEBUG"
    log_status "$MESSAGE" "$STATUS"
}

log_ok () {

    MESSAGE="$1"
    STATUS="OK"
    log_status "$MESSAGE" "$STATUS"
}

log_not_ok () {

    MESSAGE="$1"
    STATUS="NOT_OK"
    log_status "$MESSAGE" "$STATUS"
}

log_fail () {

    MESSAGE="$1"
    STATUS="FAILED"
    log_status "$MESSAGE" "$STATUS"
}

log_success () {
    MESSAGE="$1"
    STATUS="SUCCESS"
    log_status "$MESSAGE" "$STATUS"
}

log_passed () {
    MESSAGE="$1"
    STATUS="PASSED"
    log_status "$MESSAGE" "$STATUS"
}

#
# This function echos a message 
# and displays the status at the end of the line.
#
# It can be used to create status messages other
# than the default messages available such as
# OK or FAIL
#


msg_status () {

    MESSAGE="$1"
    STATUS="$2"

    export DONOTLOG="yes"
    log_status "$MESSAGE" "$STATUS"
    msg "$MESSAGE"
    display_status "$STATUS"
    export DONOTLOG="no"
}

#
# The following functions are shorthand for 
# msg_status "a message" OK 
# msg_status "another message" FAIL
#
# Same for log functions.
#


msg_emergency () {

    MESSAGE="$1"
    STATUS="EMERGENCY"
    msg_status "$MESSAGE" "$STATUS"
}

msg_alert () {

    MESSAGE="$1"
    STATUS="ALERT"
    msg_status "$MESSAGE" "$STATUS"
}

msg_critical () {

    MESSAGE="$1"
    STATUS="CRITICAL"
    msg_status "$MESSAGE" "$STATUS"
}

msg_error () {

    MESSAGE="$1"
    STATUS="ERROR"
    msg_status "$MESSAGE" "$STATUS"
}

msg_warning () {

    MESSAGE="$1"
    STATUS="WARNING"
    msg_status "$MESSAGE" "$STATUS"
}

msg_notice () {
    MESSAGE="$1"
    STATUS="NOTICE"
    msg_status "$MESSAGE" "$STATUS"
}

msg_info () {
    MESSAGE="$1"
    STATUS="INFO"
    msg_status "$MESSAGE" "$STATUS"
}

msg_debug () {
    MESSAGE="$1"
    STATUS="DEBUG"
    msg_status "$MESSAGE" "$STATUS"
}

msg_ok () {

    MESSAGE="$1"
    STATUS="OK"
    msg_status "$MESSAGE" "$STATUS"
}

msg_not_ok () {

    MESSAGE="$1"
    STATUS="NOT_OK"
    msg_status "$MESSAGE" "$STATUS"
}

msg_fail () {

    MESSAGE="$1"
    STATUS="FAILED"
    msg_status "$MESSAGE" "$STATUS"
}

msg_success () {
    MESSAGE="$1"
    STATUS="SUCCESS"
    msg_status "$MESSAGE" "$STATUS"
}

msg_passed () {
    MESSAGE="$1"
    STATUS="PASSED"
    msg_status "$MESSAGE" "$STATUS"
}

check_status () {

    CMD="$1"
    STATUS="$2"

    if [ "$STATUS" == "0" ]
    then
        msg_ok "$CMD"
    else
        msg_fail "$CMD"
    fi
}

#
# Private function
#
# This is a function that just positions
# the cursor one row up and to the right.
# It then prints a message with specified
# Color
# It is used for displaying colored status messages on the
# Right side of the screen.
#
# ARG1 = "status message (OK / FAIL)"
# ARG2 = The color in which the status is displayed.
#
raw_status () {

    STATUS="$1"
    COLOR="$2"

    function position_cursor () {

        let RES_COL=`tput cols`-12
        tput cuf $RES_COL
        tput cuu1
    }

    position_cursor
    echo -n "["
    $DEFAULT
    $BOLD
    $COLOR
    echo -n "$STATUS"
    $DEFAULT
    echo "]"
}

#
# This function converts a status message to a particular color.
#
display_status () {


    STATUS="$1"

    case $STATUS in 

    EMERGENCY )
            STATUS="EMERGENCY"
            COLOR="$RED"
            ;;
    ALERT )
            STATUS="  ALERT  "
            COLOR="$RED"
            ;;
    CRITICAL )
            STATUS="CRITICAL "
            COLOR="$RED"
            ;;
    ERROR )
            STATUS="  ERROR  " 
            COLOR="$RED"
            ;;

    WARNING )
            STATUS=" WARNING "  
            COLOR="$YELLOW"
            ;;

    NOTICE )
            STATUS=" NOTICE  "  
            COLOR="$BLUE"
            ;;
    INFO )
            STATUS="  INFO   "  
            COLOR="$CYAN"
            ;;
    DEBUG )
            STATUS="  DEBUG  "
            COLOR="$DEFAULT"
            ;;    

    OK  ) 
            STATUS="   OK    "  
            COLOR="$GREEN"
            ;;
    NOT_OK)
            STATUS=" NOT OK  "
            COLOR="$RED"
            ;;

    PASSED ) 
            STATUS=" PASSED  "  
            COLOR="$GREEN"
            ;;

    SUCCESS ) 
            STATUS=" SUCCESS "  
            COLOR="$GREEN"
            ;;
    
    FAILURE | FAILED )
            STATUS=" FAILED  "  
            COLOR="$RED"
            ;;

    *)
            STATUS="UNDEFINED"
            COLOR="$YELLOW"
    esac

    raw_status "$STATUS" "$COLOR"
}

#
# Exit with error status 
#
bail () {

    ERROR="$?"
    MSG="$1"
    if [ ! "$ERROR" = "0" ]
    then
        msg_fail "$MSG"
        exit "$ERROR"
    fi
}

#
# This function executes a command provided as a parameter
# The function then displays if the command succeeded or not.
#
cmd () {

    COMMAND="$1"
    msg "Executing: $COMMAND"

    RESULT=`$COMMAND 2>&1`
    ERROR="$?"

    MSG="Command: ${COMMAND:0:29}..."
    
    tput cuu1

    if [ "$ERROR" == "0" ]
    then
        msg_ok "$MSG"
        if [ "$DEBUG" == "1" ]
        then
            msg "$RESULT"
        fi
    else
        msg_fail "$MSG"
        log "$RESULT"
    fi

    return "$ERROR"
}

#
# These functions can be used for timing how long (a) command(s) take to
# execute.
#
now () {

   echo $(date +%s)
}

elapsed () {
    
    START="$1"
    STOP="$2"

    ELAPSED=$(( STOP - START ))
    msg "Seconds elapsed: $ELAPSED"
}

start_watch () {

    START_WATCH=`now`
}

stop_watch () {

    if has_value START_WATCH
    then
        STOP_WATCH=`now`
        elapsed "$START_WATCH" "$STOP_WATCH"
    else
        msg "Start time not set... (start_watch)"
    fi
}

#
# Prints an error message ($2) to stderr and exits with the return code ($1).
# The message is also logged.
#
function die {
    local -r err_code="$1"
    local -r err_msg="$2"
    local -r err_caller="${3:-$(caller 0)}"

    msg_fail "ERROR: $err_msg"
    msg_fail "ERROR: At line $err_caller"
    msg_fail "ERROR: Error code = $err_code"
    exit "$err_code"
} >&2 # function writes to stderr

#
# Check if a return code ($1) indicates an error (i.e. >0) and prints an error
# message ($2) to stderr and exits with the return code ($1).
# The error is also logged.
#
# Die if error code is false.
#
function die_if_false {
    local -r err_code=$1
    local -r err_msg=$2
    local -r err_caller=$(caller 0)

    if [[ "$err_code" != "0" ]]
    then
        die $err_code "$err_msg" "$err_caller"
    fi
} >&2 # function writes to stderr

#
# Dies when error code is true
#
function die_if_true {
    local -r err_code=$1
    local -r err_msg=$2
    local -r err_caller=$(caller 0)

    if [[ "$err_code" == "0" ]]
    then
        die $err_code "$err_msg" "$err_caller"
    fi
} >&2 # function writes to stderr


# private: don't use
__bsfl_array_append () {
    echo -n 'eval '
    echo -n "$1" # array name
    echo -n '=( "${'
    echo -n "$1"
    echo -n '[@]}" "'
    echo -n "$2" # item to append
    echo -n '" )'
}

# private: don't use
__bsfl_array_append_first () {
    echo -n 'eval '
    echo -n "$1" # array name
    echo -n '=( '
    echo -n "$2" # item to append
    echo -n ' )'
}

# private: don't use
__bsfl_array_len () {
    echo -n 'eval local '
    echo -n "$1" # variable name
    echo -n '=${#'
    echo -n "$2" # array name
    echo -n '[@]}'
}

#
# Appends one or more items to an array. First parameter is array's name.
#
array_append () {
    local array=$1; shift 1

    $(__bsfl_array_len len $array)

    if (( len == 0 )); then
        $(__bsfl_array_append_first $array "$1" )
        shift 1
    fi

    local i
    for i in "$@"; do
        $(__bsfl_array_append $array "$i")
    done
}

#
# Returns the size of an array.
#
array_size () {
   
    $(__bsfl_array_len size $1)
    echo "$size"
}

#
# Handy for debugging, print the contents of an array.
#
array_print () {

  eval "printf '%s\n' \"\${$1[@]}\""

}

#
# Replace some text inside a string.
#
function str_replace () {
    local ORIG="$1"
    local DEST="$2"
    local DATA="$3"

    echo "$DATA" | sed "s/$ORIG/$DEST/g"
}

#
#  Priveate, do not use (internal to stack usage)
#
declare TMP_STACK

__stack_push_tmp () {

    local TMP="$1"

    if has_value TMP_STACK
    then
        TMP_STACK="$TMP"
    else
        TMP_STACK="$TMP_STACK"$'\n'"$TMP"
    fi
}

#
# Implementation of stack. 
# Supports push and pop (add item/get+remove item)
#

declare STACK

stack_push () {

    line="$1"
        
    if has_value $STACK 
    then    
        STACK="$line"
    else    
        STACK="$line"$'\n'"$STACK"
    fi
}

stack_pop () {

    TMP_STACK=""
    i=0 
    tmp=""
    for x in $STACK
    do
        if [ "$i" == "0" ]
        then
            tmp="$x"
        else
            stack_push_tmp "$x"
        fi
        ((i++))
    done
    STACK="$TMP_STACK"
    REGISTER="$tmp"
    if [ -z "$REGISTER" ]
    then
        return 1
    else
        return 0
    fi
}


#
# Replace string of text in file.
# Uses the ed editor to replace the string.
#
# arg1 = string to be matched
# arg2 = new string that replaces matched string
# arg3 = file to operate on.
#
function str_replace_in_file () {
    local ORIG="$1"
    local DEST="$2"
    local FILE="$3"

    has_value FILE 
    die_if_false $? "Empty argument 'file'"
    file_exists "$FILE"
    die_if_false $? "File does not exist"

    printf ",s/$ORIG/$DEST/g\nw\nQ" | ed -s "$FILE" > /dev/null 2>&1
    return "$?"
}

init

